-
Notifications
You must be signed in to change notification settings - Fork 731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better edit (replace handling) #7594
Conversation
fe947ca
to
6d56967
Compare
@@ -50,5 +50,6 @@ import com.squareup.moshi.JsonClass | |||
data class AggregatedRelations( | |||
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, | |||
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null, | |||
@Json(name = "m.replace") val replaces: AggregatedReplace? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we may need to update the related Kdoc. And it seems the preview of this doc in the IDE is not working, the <code></code>
seems to not be well rendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, should use ```
val algorithm: String, | ||
) | ||
|
||
fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this method should be in EventExt
file instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
val algorithm: String, | ||
) | ||
|
||
fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should try to add unit tests on this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>() | ||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() | ||
// XXX | ||
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this description comment! Indeed, when adding the implementation for beacon events, I was concerned about that. But I couldn't find any other solution since the timeline mecanism seems to rely a lot on MessageContent
... Do you know if there is any example of events like these which are correctly implemented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that they should not be at all MessageContent from the beginning. could use MessageRelationContent though?
Where is it causing problem?
@@ -36,13 +37,22 @@ internal object EventAnnotationsSummaryMapper { | |||
) | |||
}, | |||
editSummary = annotationsSummary.editSummary | |||
?.let { | |||
val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null | |||
?.let { summary -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should take the occasion to introduce a dedicated EditAggregatedSummaryMapper
(as a classical class and not an Object
). We could then add unit tests on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted as object and added tests
* if two or more replacement events have identical origin_server_ts, | ||
* the event with the lexicographically largest event_id is treated as more recent. | ||
*/ | ||
val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure the implementation is correct. I think the natural order of sorting will be the increasing order and here we would like decreasing order so that we get the most recent first. And I am also not sure about the default sorting for strings.
This is one of the reasons, I strongly think we need unit tests to validate the implementation.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added tests
*/ | ||
val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId }) | ||
.lastOrNull() ?: return@let null | ||
// get the event and validate? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there this comment? Is there a validation step missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no should be validated before, or for late decryption after
@@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8 | |||
|
|||
override fun doMigrate(realm: DynamicRealm) { | |||
val editionOfEventSchema = realm.schema.create("EditionOfEvent") | |||
.addField(EditionOfEventFields.CONTENT, String::class.java) | |||
.addField("content"/**EditionOfEventFields.CONTENT*/, String::class.java) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should add a comment about this commented code? I guess you wanted to keep the previous fields in comments for future reference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept it to show that it was from an added then deleted class, ok to remove though
@@ -40,7 +41,8 @@ fun TimelineEvent.getVectorLastMessageContent(): MessageContent? { | |||
// Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method | |||
return when (root.getClearType()) { | |||
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { | |||
(annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageVoiceBroadcastInfoContent>() | |||
(annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>().toContent().toModel<MessageVoiceBroadcastInfoContent>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could replace this by:
getLastEditNewContent().toModel<MessageVoiceBroadcastInfoContent>()
? The difference is that in this case we would use the newContent
field instead of toContent()
extension method.
@@ -55,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor( | |||
private val reactionsSummaryFactory: ReactionsSummaryFactory | |||
) { | |||
|
|||
fun create(params: TimelineItemFactoryParams): MessageInformationData { | |||
fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure it is needed to add the lastEdit
as a new argument. Since it is already contained in the first argument, I think we could retrieve it easily inside this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
val transaction = slot<suspend (Realm) -> T>() | ||
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { | ||
secondArg<suspend (Realm) -> T>().invoke(realm) | ||
val transaction = slot<(Realm) -> T>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized the capture of the second argument is in fact not necessary when mocking the answer, so we could simplify this by:
fun <T> givenAwaitTransaction(realm: Realm) {
coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers {
secondArg<(Realm) -> T>().invoke(realm)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent | ||
|
||
data class TimelineItemFactoryParams( | ||
val event: TimelineEvent, | ||
val lastEdit: Event? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This field is not used: maybe it should be used inside MessageInformationDataFactory.create()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's used now
private val clock: Clock, | ||
) : EventInsertLiveProcessor { | ||
|
||
private val allowedTypes = listOf( | ||
EventType.MESSAGE, | ||
EventType.REDACTION, | ||
EventType.REACTION, | ||
// The aggregator handles verification events but just to render tiles in the timeline | ||
// It's not participating in verfication it's self, just timeline display | ||
EventType.KEY_VERIFICATION_DONE, | ||
EventType.KEY_VERIFICATION_CANCEL, | ||
EventType.KEY_VERIFICATION_ACCEPT, | ||
EventType.KEY_VERIFICATION_START, | ||
EventType.KEY_VERIFICATION_MAC, | ||
// TODO Add ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we remove the // TODO Add ?
since it seems to be done now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
private val clock: Clock, | ||
) : EventInsertLiveProcessor { | ||
|
||
private val allowedTypes = listOf( | ||
EventType.MESSAGE, | ||
EventType.REDACTION, | ||
EventType.REACTION, | ||
// The aggregator handles verification events but just to render tiles in the timeline | ||
// It's not participating in verfication it's self, just timeline display |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// It's not participating in verfication it's self, just timeline display | |
// It's not participating in verification itself, just timeline display |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -334,9 +309,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( | |||
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") | |||
existingSummary.editions.add( | |||
EditionOfEvent( | |||
senderId = event.senderId ?: "", | |||
eventId = event.eventId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eventId = event.eventId, | |
eventId = eventId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -369,8 +343,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( | |||
* @param editions list of edition of event | |||
*/ | |||
private fun handleThreadSummaryEdition( | |||
editedEvent: EventEntity?, | |||
replaceEvent: TimelineEventEntity?, | |||
editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is better to have one argument per line for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have posted some comments concerning some implementation details that should be investigated I guess.
After some tests, here the issues I found:
- in non encrypted room, the last edited content is not rendered in the timeline for a text message for example. This is always the original content which is rendered
- in encrypted room, not really sure but I think we might no more render the local echo of the edited content in the timeline as I have seen some short delay before rendering (it may wait for the remote content before rendering).
I have not tested the update of the app for migration of the DB. If not already done, I think the migration should be tested before merge.
0feb4ed
to
7eb595b
Compare
* limitations under the License. | ||
*/ | ||
|
||
package org.matrix.android.sdk.api.session.events.model |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were you aware there was an existing EventExt
in the org.matrix.android.sdk.internal.session.events
package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but it's internal, and toValidate is API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I see. Thanks for explaining.
|
||
class ValidDecryptedEventTest { | ||
|
||
val fakeEvent = Event( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should declare the variable as private
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent | ||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent | ||
|
||
class ValidDecryptedEventTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding these tests!
import org.matrix.android.sdk.internal.database.model.EditionOfEvent | ||
import org.matrix.android.sdk.internal.database.model.EventEntity | ||
|
||
class EditAggregationSummaryMapperTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename it EditAggregatedSummaryEntityMapperTest
? It took me while to find the corresponding tests and IDE does not recognize the associations between the class and the test if naming is not aligned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
class EditAggregationSummaryMapperTest { | ||
|
||
@Test | ||
fun test() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a test when we have same timestamp for 2 EditionOfEvent
to check we take the correct latest event in this situation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea, added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates. I didn't expect android tests (was thinking about manual tests) on database migration but that's awesome we know have tests on this.
I have just added some small new comments. I wonder also you have seen some of my previous comments (and particularly the ones about EventEditValidatorTest
)?
Yes, that's nice, @BillCarsonFr can you explain somewhere how to create files like |
cd497b2
to
bec8b5f
Compare
I launched the app in the emulator (with And I added that to test assets. We should create a task to do that for crypto and auth (and probably a better one for session) |
I want to handle them in another PR, to rationalize test on IMXCryptoStore and have a bunch of fake events in test.fixtures |
SonarCloud Quality Gate failed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Type of change
Content
This PR is improving the handling of Edits, and enforcing edit validation rules.
Edit validation: see
EventEditValidator
ensures the rules as per spec (recently updated)Added proper unit tests
Persistence layer changes:
lastEdit
instead ofLastContent
(as sender/types are also needed for validation)Fixed a problem in Aggregator with
process
calls beeing suspend, which is a problem as it's called within a realm transaction and realm instance is locked to a thread. AllasyncTransaction
helpers have been modified to make theblock
param not suspendableHandling of server side aggregation is still not handled as there is ongoing discussions regarding that
Motivation and context
#7430
Screenshots / GIFs
N/A
Tests
Try some edits, polls, location sharing
Tested devices
Checklist